Skip to content

feat: read the engine's unfulfilled-keys report instead of diffing declared vs delivered - #1308

Draft
ralphstodomingo wants to merge 10 commits into
mainfrom
feat/unfulfilled-keys-meta
Draft

ralphstodomingo wants to merge 10 commits into
mainfrom
feat/unfulfilled-keys-meta

Conversation

@ralphstodomingo

@ralphstodomingo ralphstodomingo commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Issue for this PR

Closes #1307

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

When a workspace is bound, the attach toast says "N of M declared integration tools available" and lists what is declared but absent. Until now that list came from a client-side diff: fetch the workspace's allowlist from the API, subtract the tool names the engine served. A diff can name keys, never reasons — an expired Jira token, an MCP server whose binary is not installed, an integration the tenant removed from the catalog, an extension tool with no VS Code window, and a key the provider does not offer all read the same.

@altimateai/datamate 0.7.2 (AltimateAI/altimate-mcp-engine#248) reports every declared-but-unserved key with a reason under _meta["ai.altimate/unfulfilled"] on each tools/list response. This PR reads it:

  • MCP catalog keeps the _meta of a server's last tools/list page per client. paginate keeps only each page's items, so the result object — the only carrier of _meta — was dropped. A listing starts with none; any page that carries one sets it; a listing without one clears it. Exposed as MCP.listMeta(name) (undefined while not connected).
  • Attach takes the gaps from the report instead of the diff. no-bridge entries stay out of the "missing" line, as absent extension tools without an IDE were already treated as expected; every other reason is named, grouped, with the engine's detail (e.g. spawn docker ENOENT) — Declared but not available — no usable connection: jira_search_issues; server failed to start (spawn docker ENOENT): gh_list_prs, gh_create_pr. The "N of M" headline still counts declared keys that are present. The attached outcome carries the full report for later surfaces.
  • No report means no claim. An engine that sends no _meta (nothing at or above the floor does) yields an outcome with neither missing nor unfulfilled, and a toast with no gap line — not "all served".
  • Floor moves to 0.7.2 (MIN_ENGINE_VERSION), the first engine that emits the report. Do not merge before @altimateai/datamate@0.7.2 is on npm (AltimateAI/altimate-mcp-engine#249 is the bump); until then every attach would refuse with "needs 0.7.2 or newer".

The client no longer reads what it cannot know: the allowlist lookup (declared()) is kept only for the headline's denominator and the extension-tool count, and a report without a reachable allowlist still names the gaps.

Claims

  • C1 — Nothing is invented. missing and unfulfilled exist on the outcome only when the engine sent a well-formed report; a malformed or absent _meta yields neither (test: "an engine that sends no report is not read as having no gaps"; parseUnfulfilled cases).
  • C2 — no-bridge never counts as missing, and every other reason does — including unknown-key on an extension key while a bridge is connected (reportedMissing; test "no-bridge entries in the report are expected, never missing").
  • C3 — The catalog keeps the report across the paths that list tools: initial connect, the tools/list_changed refresh, the post-OAuth reconnect all go through McpCatalog.defslistTools, which is the only writer (catalog-list-meta.test.ts covers first page, multi-page, and clearing).
  • C4 — A gap whose reason changes is announced again; an identical report is not (signature carries key=reason; test "a gap whose reason changed is announced again").
  • C5 — Servers other than the engine are unaffected: _meta is retained per client but read only for datamate; tool conversion and the stored defs are unchanged.

Residuals

  • R1 — Reasons outside the engine's current set are shown verbatim (a newer engine may add one) rather than dropped.
  • R2 — The toast shows at most five keys across groups and truncates a detail at 60 characters; the full report is on the outcome.
  • R3 — The floor bump refuses 0.7.1 engines; that is the intended contract, and the reason is in the MIN_ENGINE_VERSION comment.

How did you verify your code works?

  • bun run typecheck clean; prettier clean on the files this PR touches (the files that were already non-conforming on main are left as they were).

  • test/altimate/workspace (all), test/mcp/catalog-list-meta.test.ts, test/altimate/precedence-guard-order.test.ts: 460 pass. test/mcp and the two session suites whose MCP stubs gained listMeta: 279 pass; the 5 mcp.headers failures and 1 oauth-auto-connect failure reproduce identically on an untouched main checkout (environmental, not this change).

  • New tests: 6 attach cases (reasons in the toast, no-bridge exclusion, no-report, report-without-allowlist, reason-change re-announce, the existing inventory case now stating the engine's report), describeMissing/parseUnfulfilled/reportedMissing unit cases, 3 catalog cases over a real in-memory MCP server.

  • End to end through the real MCP service (test/mcp/engine-unfulfilled.e2e.test.ts, env-guarded, skipped in CI): the engine at AltimateAI/altimate-mcp-engine#248's head built as 0.7.2 is spawned over stdio by MCP.add exactly as the overlay spawns it, against a fake Altimate API, a real second MCP server and a missing binary; MCP.listMeta("datamate") returns the five-entry report with the expected reasons and the toast text reads Declared but not available — no usable connection: jira_search_issues; not offered by the integration: ghost; server failed to start (spawn altimate-e2e-missing-binary ENOENT): whatever; no longer in the catalog: retired_tool. — 1 pass. Run it with ALTIMATE_ENGINE_E2E_ROOT=<engine checkout with dist/> bun test test/mcp/engine-unfulfilled.e2e.test.ts from packages/opencode.

  • Engine → CLI through the real attach path (evidence): bootstrap + beforeTurn on a bound directory against the 0.7.2 release candidate (engine PRs 250 + 248 merged, built locally, on PATH as datamate). Settled outcome attached with declared: 5, missing: [jira_search_issues, ghost, whatever, retired_tool], the full report incl. the no-bridge entry, and the exact toast text; 8/8 checks. A 0.7.1 build is refused as engine-too-old with the install line; 2/2.

Screenshots / recordings

Not a UI change beyond toast text; the exact strings are asserted in the tests above.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

🤖 Generated with Claude Code

https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b

Appendix — complexity delta (altimate-code: engine unfulfilled report)

e8c21c2af793c879af8 · only functions this diff touches · advisory, not a gate.

✅ No touched function changed in complexity (12 touched, 4 new, all under 10).

ℹ️ How to read these numbers

Cognitive (Sonar spec) counts breaks in linear reading flow — each if/loop/catch/ternary/boolean-operator switch adds 1, and nesting makes every further break cost more. It approximates how much you must hold in your head to follow the function: 0–5 trivial · 6–10 easy · 11–15 moderate (15 = Sonar's recommended per-function cap) · 16–25 hard to follow · >25 needs decomposition.

CCN (cyclomatic) counts independent paths — also the minimum number of test cases for full branch coverage of the function.

Only functions this diff touches are measured, as deltas — pre-existing complexity is not counted against this change. Rising numbers aren't automatically wrong; they're where review attention should go. Test files excluded.


Summary by cubic

Closes #1307. Workspace attach now reads the engine’s ai.altimate/unfulfilled report instead of diffing declared and delivered tools, so missing-tool notices include actionable reasons and details rather than only tool names.

Attach behavior

  • Excludes no-bridge entries; every other valid reason is reported.
  • Groups missing tools by reason and integration, preserving the engine’s detail.
  • Leaves missing and unfulfilled unset when the report is absent or malformed.
  • Re-announces a gap when its reason changes, but not when the report is identical.
  • Counts distinct sanitized catalog entries, including collisions across ordinary and extension tools.
  • Accepts numeric integration IDs and stores them as strings.

MCP catalog

  • Preserves each client’s tools/list _meta and exposes it through MCP.listMeta(name).
  • Commits tools and their report together through MCP.snapshot(name) to prevent mismatched refreshes.
  • Keeps the previous report for pending or failed listings; a completed listing without _meta clears it.
  • Raises MIN_ENGINE_VERSION to 0.7.2, which requires @altimateai/datamate 0.7.2 or newer.

Written for commit ef0b8ed. Summary will update on new commits.

Review in cubic

ralphstodomingo and others added 2 commits September 12, 2026 08:17
…clared vs delivered

The MCP catalog now keeps the `_meta` of a server's last tools/list page per client, exposed as
`MCP.listMeta(name)`. On attach, the gaps come from the engine's `ai.altimate/unfulfilled` report,
grouped by reason in the toast and headless line with the engine's detail (e.g. `spawn docker ENOENT`);
`no-bridge` entries stay out of the missing set as before. The attached outcome carries the full
report. `MIN_ENGINE_VERSION` moves to 0.7.2, the first engine that emits it; an engine that sends
none claims no gaps rather than inventing them.

Closes #1307

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
Env-guarded (`ALTIMATE_ENGINE_E2E_ROOT`), skipped otherwise: spawns a built engine over stdio the way
the overlay does, against a fake Altimate API and a real second MCP server, and reads the
`ai.altimate/unfulfilled` report through `MCP.listMeta` into the attach toast text. The engine is a
node shebang script, so the test spawns node rather than the bun test runner.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
@ralphstodomingo ralphstodomingo self-assigned this Sep 12, 2026
@coderabbitai

coderabbitai Bot commented Sep 12, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Marker Guard flagged the changed lines in the upstream-shared catalog; the single-line marker
comments did not count as a wrapped block.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
@ralphstodomingo

Copy link
Copy Markdown
Contributor Author

Engine → altimate-code, through the real attach path

This runs the CLI's production attach code under a real instance with no model turn: the binding cache, the datamate lookup on PATH, the --version probe, the allowlist lookup, the MCP spawn, the report and the toast are all the real code (bootstrap(dir, …)beforeTurn(sessionID)). Only two things are not production: the SaaS API is a local fake (it serves both the CLI's endpoints and the engine's, and every call is asserted 200), and the toast sink is captured instead of published to a TUI bus. The engine is the 0.7.2 release candidate: AltimateAI/altimate-mcp-engine#250's head with AltimateAI/altimate-mcp-engine#248 merged in, built locally (datamate --version0.7.2), reached through a shim on PATH exactly as a global install would be.

Declared by the workspace: jira (no connection on this machine), vscode-power-user (extension), mcp-ok (a real second MCP server offering only echo), mcp-missing-binary (command absent), retired-integration (not in the catalog).

Result — release candidate (head engine-rc-0.7.2, 6170 ms from beforeTurn to settled outcome)

Toast (variant warning):

1 of 5 declared integration tools available. Declared but not available — no usable connection: jira_search_issues; not offered by the integration: ghost; server failed to start (spawn altimate-e2e-missing-binary ENOENT): whatever; no longer in the catalog: retired_tool.

Settled outcome:

{
  "kind": "attached",
  "available": 1,
  "declared": 5,
  "missing": [
    "jira_search_issues",
    "ghost",
    "whatever",
    "retired_tool"
  ],
  "unfulfilled": [
    {
      "key": "jira_search_issues",
      "integrationId": "jira",
      "reason": "invalid-connection"
    },
    {
      "key": "pu_lineage",
      "integrationId": "vscode-power-user",
      "reason": "no-bridge"
    },
    {
      "key": "ghost",
      "integrationId": "mcp-ok",
      "reason": "unknown-key"
    },
    {
      "key": "whatever",
      "integrationId": "mcp-missing-binary",
      "reason": "spawn-failed",
      "detail": "spawn altimate-e2e-missing-binary ENOENT"
    },
    {
      "key": "retired_tool",
      "integrationId": "retired-integration",
      "reason": "catalog-missing"
    }
  ]
}
  • PASS outcome is attached
  • PASS declared counted from the allowlist (5 CLI-servable keys)
  • PASS missing = every gap the engine reported except no-bridge
  • PASS outcome carries the full report incl. the no-bridge entry
  • PASS spawn-failed detail carries the engine's error
  • PASS exactly one toast, warning
  • PASS toast text
  • PASS every API call served (no 404)

API calls, in order: GET /skills -> 200, GET /datamates/77/summary -> 200, GET /datamate_integrations/ -> 200, GET /dbt/v3/validate-credentials -> 200, GET /datamates -> 200, GET /datamate_integrations -> 200, GET /mask -> 200, GET /datamate_integrations/custom -> 200, GET /connections -> 200 — the first three are the CLI's, the rest the engine's.

Result — floor negative (engine 0.7.1, headless)

The same run against a 0.7.1 build settles engine-too-old (found 0.7.1) and prints one stderr line:

Workspace "e2e-rc": 5 integration tools need datamate 0.7.2+ (found 0.7.1). Update with: npm i -g @altimateai/datamate@0.7.2

  • PASS engine below the floor is refused as engine-too-old
  • PASS refusal names the found version and the floor

Not covered

Windows; a real tenant; the TUI rendering of the toast (the text is asserted, the widget is not).

Reproduce — .e2e/engine-to-cli.ts + .e2e/run.sh (run from packages/opencode)
.e2e/run.sh <engine checkout with dist/> out.json            # attach
HEADLESS=1 .e2e/run.sh <0.7.1 engine> out.json too-old      # floor negative

run.sh (starts bun with an isolated HOME — Bun caches os.homedir() at startup, so the script cannot set it itself):

#!/usr/bin/env bash
# usage: .e2e/run.sh <engine-root> <out.json> [EXPECT]
set -uo pipefail
H=$(mktemp -d /tmp/e2e-engine-to-cli-home-XXXXXX)
env -i HOME="$H" PATH="$H/bin:/usr/local/bin:/usr/bin:/bin:$(dirname "$(command -v bun)"):$(dirname "$(command -v node)")" TERM=dumb ALTIMATE_WORKSPACE=1 ${HEADLESS:+ALTIMATE_CODE_HEADLESS=1} ENGINE_ROOT="$1" EXPECT="${3:-}" \
  timeout 180 bun .e2e/engine-to-cli.ts > "$2" 2> "${2%.json}.stderr"
echo "RC=$? HOME=$H"

engine-to-cli.ts:

// Engine → altimate-code, through the CLI's real attach path. Nothing is
// stubbed except the toast sink (so its text can be captured) and the SaaS
// API (served locally). The binding, the `datamate` lookup on PATH, the
// version probe, the allowlist lookup, the MCP spawn and the report all run
// the production code under a real instance — no model turn.
import http from "node:http"
import path from "node:path"
import { mkdtempSync, mkdirSync, writeFileSync, chmodSync } from "node:fs"
import { tmpdir } from "node:os"
import { execFileSync } from "node:child_process"

const engineRoot = process.env["ENGINE_ROOT"]!
const node = Bun.which("node")!
const DATAMATE_ID = 77

// ---- fake SaaS: engine endpoints + the CLI's own ----------------------------
const catalog = [
  { id: "jira", type: "tool", name: "Jira", description: "", url: "", supportsLocalConnectionTest: true, supportsSaasConnectionTest: false,
    config: [{ key: "url", name: "URL", type: "string", required: true }, { key: "email", name: "Email", type: "string", required: true }, { key: "token", name: "Token", type: "string", required: true }],
    tools: [{ key: "jira_search_issues", name: "Search issues" }] },
  { id: "vscode-power-user", type: "extension", name: "Power User for dbt", description: "", url: "", supportsLocalConnectionTest: false, supportsSaasConnectionTest: false, config: [],
    tools: [{ key: "pu_lineage", name: "Lineage" }] },
]
const custom = [
  { id: "mcp-ok", type: "mcp", name: "Echo MCP", description: "", url: "", config: [],
    toolConfig: [{ key: "type", name: "type", type: "string", required: false, value: "stdio" }, { key: "command", name: "command", type: "string", required: true, value: node },
      { key: "arguments", name: "arguments", type: "array", required: false, value: [path.join(import.meta.dir, "../test/mcp/fixtures/echo-mcp-server.mjs")] }],
    tools: [{ key: "echo" }, { key: "ghost" }] },
  { id: "mcp-missing-binary", type: "mcp", name: "Missing MCP", description: "", url: "", config: [],
    toolConfig: [{ key: "type", name: "type", type: "string", required: false, value: "stdio" }, { key: "command", name: "command", type: "string", required: true, value: "altimate-e2e-missing-binary" }],
    tools: [{ key: "whatever" }] },
]
const datamate = {
  id: String(DATAMATE_ID), name: "e2e-rc", description: "", privacy: "private", memory_enabled: false, knowledge_engine_enabled: false, knowledge_bases: [],
  integrations: [
    { id: "jira", type: "tool", name: "Jira", description: "", url: "", tools: [{ key: "jira_search_issues" }] },
    { id: "vscode-power-user", type: "extension", name: "PU", description: "", url: "", tools: [{ key: "pu_lineage" }] },
    { id: "mcp-ok", type: "mcp", name: "Echo MCP", description: "", url: "", tools: [{ key: "echo" }, { key: "ghost" }] },
    { id: "mcp-missing-binary", type: "mcp", name: "Missing MCP", description: "", url: "", tools: [{ key: "whatever" }] },
    { id: "retired-integration", type: "tool", name: "Retired", description: "", url: "", tools: [{ key: "retired_tool" }] },
  ],
}
const hits: string[] = []
const api = http.createServer((req, res) => {
  const url = new URL(req.url ?? "/", "http://x"); const p = url.pathname.replace(/\/+$/, "") || "/"
  const json = (code: number, body?: unknown) => { hits.push(`${req.method} ${url.pathname} -> ${code}`); res.writeHead(code, { "content-type": "application/json" }); res.end(body === undefined ? "" : JSON.stringify(body)) }
  if (p === "/dbt/v3/validate-credentials") return json(200, { ok: true })
  if (p === "/datamates") return json(200, { datamates: [datamate] })
  if (p === `/datamates/${DATAMATE_ID}/summary`) return json(200, { datamate })
  if (p === "/datamate_integrations") return json(200, catalog)
  if (p === "/datamate_integrations/custom") return json(200, { items: custom })
  if (p === "/mask") return json(200, { mask_data: [] })
  if (p === "/connections") return json(200, { connections: [] })
  if (p === `/datamates/${DATAMATE_ID}/knowledge_bases`) return json(200, { knowledge_bases: [] })
  if (p === `/datamates/${DATAMATE_ID}/knowledge_engine_description`) return json(200, {})
  if (p === "/datamates/audit/create_batch") return json(204)
  if (p === "/skills") return json(200, { skills: [] })
  return json(404, { detail: `unhandled ${p}` })
})
await new Promise<void>((r) => api.listen(0, "127.0.0.1", r))
const apiUrl = `http://127.0.0.1:${(api.address() as { port: number }).port}`

// ---- isolated HOME: the wrapper starts this process with HOME already pointing
// at a fresh directory (Bun caches os.homedir() at startup, so setting it here
// would be too late for Global.Path); this script only fills it in.
const home = process.env["HOME"]!
if (!home.includes("e2e-engine-to-cli-home-")) throw new Error(`refusing to run against a real HOME: ${home}`)
mkdirSync(path.join(home, ".altimate"), { recursive: true })
writeFileSync(path.join(home, ".altimate/altimate.json"), JSON.stringify({ altimateUrl: apiUrl, altimateInstanceName: "e2e", altimateApiKey: "e2e-key" }))
writeFileSync(path.join(home, ".altimate/settings.json"), "{}")
writeFileSync(path.join(home, ".altimate/connections.json"), "[]")
// `datamate` on PATH → the engine under test, on node (the published bin is a node shebang script)
const bin = path.join(home, "bin"); mkdirSync(bin)
writeFileSync(path.join(bin, "datamate"), `#!/bin/sh\nexec "${node}" "${path.join(engineRoot, "dist/cli.js")}" "$@"\n`); chmodSync(path.join(bin, "datamate"), 0o755)
process.env["PATH"] = `${bin}:${process.env["PATH"]}`
process.env["ALTIMATE_WORKSPACE"] = "1"
const resolved = Bun.which("datamate")
const engineVersion = execFileSync(resolved!, ["--version"], { encoding: "utf8" }).trim().split("\n").pop()

// a bound project directory
const project = mkdtempSync(path.join(tmpdir(), "e2e-engine-to-cli-project-"))
execFileSync("git", ["init", "-q", project])

// ---- production modules, imported only after the environment is shaped ----
const { bootstrap } = await import("../src/cli/bootstrap")
const { recordApprovedBinding } = await import("../src/altimate/workspace/state")
const { beforeTurn, settledOutcome } = await import("../src/altimate/workspace/engine-overlay")
const { syncInternals } = await import("../src/altimate/workspace/engine-seams")
const toasts: { title: string; message: string; variant: string }[] = []
syncInternals.notify = async (t) => { toasts.push(t) }   // capture only; no TUI bus here
const lines: string[] = []
syncInternals.printLine = (l) => { lines.push(l) }

await recordApprovedBinding(project, { datamateId: DATAMATE_ID, datamateName: "e2e-rc", repoRemote: null, projectPath: project, linkedAt: Date.now() })
const t0 = Date.now()
const result = await bootstrap(project, async () => {
  await beforeTurn("s1")
  return settledOutcome("s1")
})
const elapsedMs = Date.now() - t0
api.close()

const checks: string[] = []
const check = (label: string, ok: boolean) => checks.push(`${ok ? "PASS" : "FAIL"} ${label}`)
const attached = result?.kind === "attached" ? result : undefined
if (process.env["EXPECT"] === "too-old") {
  check("engine below the floor is refused as engine-too-old", result?.kind === "engine-too-old")
  check("refusal names the found version and the floor", lines.concat(toasts.map((t) => t.message)).some((l) => l.includes(engineVersion!) && l.includes("0.7.2")))
} else {
  check("outcome is attached", !!attached)
  check("declared counted from the allowlist (5 CLI-servable keys)", attached?.declared === 5)
  check("missing = every gap the engine reported except no-bridge", JSON.stringify(attached?.missing) === JSON.stringify(["jira_search_issues", "ghost", "whatever", "retired_tool"]))
  check("outcome carries the full report incl. the no-bridge entry", !!attached?.unfulfilled?.some((u) => u.key === "pu_lineage" && u.reason === "no-bridge"))
  check("spawn-failed detail carries the engine's error", /ENOENT/.test(attached?.unfulfilled?.find((u) => u.key === "whatever")?.detail ?? ""))
  check("exactly one toast, warning", toasts.length === 1 && toasts[0].variant === "warning")
  check("toast text", toasts[0]?.message === "1 of 5 declared integration tools available. Declared but not available — no usable connection: jira_search_issues; not offered by the integration: ghost; server failed to start (spawn altimate-e2e-missing-binary ENOENT): whatever; no longer in the catalog: retired_tool.")
  check("every API call served (no 404)", !hits.some((h) => / -> 404$/.test(h)))
}
console.log(JSON.stringify({ engineRoot, home, resolvedDatamate: resolved, engineVersion, elapsedMs, outcome: result, toasts, lines, apiHits: hits, checks, verdict: checks.every((c) => c.startsWith("PASS")) ? "ALL PASS" : "FAILURES" }, null, 2))
process.exit(checks.every((c) => c.startsWith("PASS")) ? 0 : 1)

Custom (tenant-created) integrations carry numeric ids; the parser treated the whole report as
malformed over that one field and the attach announced no gaps at all. Take the id as a string.
Found by the engine-to-CLI run against a local backend with a custom MCP integration.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
@ralphstodomingo

Copy link
Copy Markdown
Contributor Author

One more commit on this head, 82531540caccept numeric integration ids in the engine report. A tenant-created (custom) MCP integration's id arrives from the engine as a number; the parser treated the whole report as malformed over that one field and the attach announced no gaps at all. The id is now taken as a string (unit test added). Found by the real-chain run for the attach-report post (#1310); the engine side stringifies too (AltimateAI/altimate-mcp-engine#248 64e5bb4), so either side alone is enough.

@saravmajestic

Copy link
Copy Markdown
Contributor

Multi-model review — client half

Reviewed jointly with AltimateAI/altimate-mcp-engine#248 as one feature. The wire contract between the two halves agrees exactly: meta key, all four field names, all six reason spellings, and the empty-array-vs-absent distinction match. parseUnfulfilled's fail-closed behaviour correctly keeps "I don't know" distinct from "nothing missing", and unknown future reasons pass through verbatim (R1). Release sequencing is tracked separately.

Major

1. Tools and their report are not committed atomically across a refresh

src/mcp/catalog.ts:166,187-190 deletes _meta when a listing starts and republishes it page by page, while defs are replaced only after the whole listing succeeds (src/mcp/index.ts:744-748).

Consequences:

  • While a refresh is pending, the previous tools stay visible but their report is gone — a turn silently loses its gap line.
  • If a later page fails, partial _meta sits beside stale defs.
  • reconcile reads the two through Promise.all (engine-overlay.ts:651), so it has no guarantee of a coherent snapshot.

This bites C3 ("the catalog keeps the report across the paths that list tools") on the tools/list_changed refresh path specifically — the report is kept across completed listings, not across a pending one.

Suggested: accumulate { tools, meta } locally and commit them together on success; retain the last good snapshot on failure; expose one snapshot accessor to the overlay.

Minor

2. The headline and the gap line can disagree (raw vs sanitized key space)

declared.keys are raw API tool.key strings (engine-probes.ts:135-137). present comes from MCP.tools() keys, which pass through sanitize = value.replace(/[^a-zA-Z0-9_-]/g, "_") (mcp/catalog.ts:145, mcp/index.ts:1081), and engineToolKeys only strips the prefix — it can't restore the original name (engine-types.ts:149-154).

So a served schema.inspect becomes datamate_schema_inspectschema_inspect and never matches the raw declaration. The headline at engine-overlay.ts:662 undercounts.

The undercount itself pre-dates this PR — the old served = declared.keys.length - missing.length used the same mismatched comparison. What changes is the presentation: previously the mismatched key was also (wrongly) named in the missing line; now the engine correctly reports nothing, so the toast reads "3 of 5 declared integration tools available." with no gap line at all. Headline and report contradict each other silently.

Suggested: normalize both sides to one key space before the present.has(k) lookup, or derive served from the report rather than from present.

3. describeMissing attributes one integration's error to another

engine-types.ts:285-303 groups solely by reason and uses the group's first non-empty detail for every key in it. Two integrations that both fail spawn-failed — one spawn docker ENOENT, one a bad path — render under a single error. integrationId is carried on every entry and ignored here, so the toast can give actively wrong repair guidance.

Suggested: group by (integrationId, reason), or only show a shared detail when every member's detail is identical.

4. "Last page's _meta" is really "last page that had one"

catalog.ts:166,187-190 clears once when the listing starts, then sets only if (result._meta !== undefined). A listing whose first page carries _meta and whose final page does not retains the first page's value — which doesn't match the stated per-page clearing. Harmless against today's single-page engine response, but ambiguous as a general accessor contract, and test/mcp/catalog-list-meta.test.ts:58 only covers _meta on the final page.

Suggested: pick a rule (last-page-authoritative vs any-page), implement it inside the completed snapshot, and add a first-page-only case.

5. Truncation is not redaction

engine-types.ts truncates detail to 60 chars into the notification, and engine-overlay.ts:685-691 logs the full report. The engine currently emits raw error.message (flagged on the engine PR); until that's bounded and sanitized upstream, sensitive text can appear at the start of a detail and survive truncation.

6. spawn-failed renders as "server failed to start"

REASON_PHRASE maps it that way, but the engine records transport construction, connect and list failures under that reason — so an auth rejection on a running server reads as a startup failure. Worth aligning the phrase with the engine's actual taxonomy.

Verified sound

  • MCP.listMeta(DATAMATE_KEY) resolves correctly — the engine is registered under DATAMATE_KEY (engine-overlay.ts:387,560,634).
  • C5 holds: _meta is retained per client via WeakMap<Client, …> and read only for datamate; a reconnect creates a new Client, so old metadata can't transfer.
  • C2 holds: reportedMissing excludes only no-bridge; unknown-key on an extension key with a connected bridge does count.
  • C1 holds: parseUnfulfilled returns undefined on malformed input, so a bad report yields neither missing nor unfulfilled.
  • C4's signature change is right — key=reason means a gap whose reason changed re-announces.
  • The served change from declared - missing to filter(present.has) is an improvement for the no-bridge case, which the old arithmetic counted as served.

…hat a gap is with its own detail

Answers the multi-model review of the unfulfilled report, client half.

- the tools of a listing and its _meta are committed in one statement (State.meta beside State.defs) and read through one accessor, MCP.snapshot(name): a refresh that is pending or that failed leaves the last good pair standing, and the overlay can no longer pair one listing's tools with another's report
- the catalog commits _meta when a listing completes — the last page that carries one wins, a listing with none clears it — instead of clearing at the start
- served counts compare the declared keys in the catalog's sanitised key space, so the headline cannot undercount a served tool whose raw key the MCP layer renamed
- the missing line groups by reason AND integration, so one integration's error is never printed as another's
- spawn-failed reads 'server could not be started or reached', which is what the engine records under it

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
@ralphstodomingo

Copy link
Copy Markdown
Contributor Author

Re-review disposition — 45a8d02c8

Answering the multi-model review (client half), paired with the engine half on AltimateAI/altimate-mcp-engine#248.

Fixed

  • Major 1 — tools and report not committed atomically. The catalog now commits _meta only when a listing completes (the last page that carries one wins; a listing with none clears it; a pending or failed listing leaves the previous value), and MCP stores it in State.meta in the same statement as State.defs. The overlay reads both through one accessor, MCP.snapshot(name), which is one synchronous pass over the state — so a refresh landing between two reads can no longer pair one listing's tools with another's report. Tests: "a _meta on the first page is kept when the last page carries none", "a listing that fails part-way leaves the previous _meta standing", plus the existing three.
  • Minor 2 — headline and gap line in different key spaces. served and extServed compare declared keys after sanitize, the same transform the MCP layer applies to tool names, so a served tool whose raw key it renamed still counts.
  • Minor 3 — one integration's error attributed to another. describeMissing groups by reason AND integration. Test: "two integrations that failed the same way keep their own details".
  • Minor 4 — "last page's _meta" semantics. Stated and tested as "the last page that carries one wins" (first-page-only case added).
  • Minor 6 — spawn-failed phrase. Now "server could not be started or reached", which covers construction, connect and list failures as the engine records them.

Recorded, not changed

Verified on this head: typecheck clean; test/mcp, test/altimate/workspace, test/altimate/plugin and the tool-race suite 735 pass. The six failures in headers.test.ts and oauth-auto-connect.test.ts fail identically on the untouched head in this environment (they need a network or a real server) and are not from this change.

@ralphstodomingo

Copy link
Copy Markdown
Contributor Author

@codex review

Scoped review against the claims below (head 45a8d02c8). For each claim, say whether the code holds it, with a concrete failing scenario where it does not. Please do not re-raise the residuals at the end.

Claims

  • C1 — parseUnfulfilled fails closed: a malformed or absent report yields undefined (neither missing nor unfulfilled on the outcome), and unknown future reasons pass through verbatim.
  • C2 — reportedMissing excludes only no-bridge; every other reason the engine reports is a gap, and an unknown-key on an extension key with a connected bridge counts.
  • C3 — The tools of a listing and its _meta are committed together and read together: State.meta[name] is written in the same statement as State.defs[name] on every path that stores a listing, deleted with it on every path that drops one, and MCP.snapshot(name) reads both in one synchronous pass. A pending or failed refresh leaves the last good pair standing.
  • C4 — The catalog commits _meta when a listing completes: the last page carrying one wins, a listing with none clears it, and a failed listing leaves the previous value.
  • C5 — served and extServed compare declared keys in the sanitised key space, so the headline and the gap line agree on a served tool whatever its raw key.
  • C6 — describeMissing groups by reason and integration; a group's detail is only ever one of its own members' details.
  • C7 — A gap whose reason changed re-announces (the reasons are in the announcement signature).

Residuals (already accepted)

  • R1 — The client truncates detail for the toast; bounding and redaction happen engine-side (AltimateAI/altimate-mcp-engine#248).
  • R2 — spawn-failed stays one reason on the wire; the phrase covers construction, connect and list failures.
  • R3 — MCP.listMeta(name) remains for callers that need only the report; the overlay uses snapshot.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 15, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-15T12:17:43.074499Z 9452109 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 45a8d02c8c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +255 to +256
if (typeof key !== "string" || typeof id !== "string" || typeof reason !== "string") return undefined
out.push({ key, integrationId: id, reason, ...(typeof detail === "string" && detail !== "" ? { detail } : {}) })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject malformed detail fields

C1 does not hold when an otherwise valid entry has a non-string detail, such as { key: "x", integrationId: "i", reason: "exception", detail: 42 }. This branch silently discards the invalid field and accepts the report, so the outcome receives both missing and unfulfilled and may announce a gap instead of failing closed with undefined.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 820147a: a present detail that is not a string fails the whole report, like the other fields. Test: "an entry with a malformed detail is a malformed report, not a report missing a field" — number, null and object details give undefined; a string, an empty string and an absent field parse.

Comment thread packages/opencode/src/mcp/index.ts Outdated
Comment on lines +762 to +763
s.defs[name] = listed
s.meta[name] = McpCatalog.listMeta(client)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Associate metadata directly with each refresh result

C3 does not hold when two ToolListChanged notifications start overlapping listings for the same client. If listing A completes and then listing B updates listMetaByClient before A's awaiting handler resumes, this lookup returns B's metadata while listed still contains A's tools; the state then commits a mismatched pair and snapshot() can produce a false or omitted gap. The listing operation needs to return its own metadata alongside its definitions rather than rereading a client-wide cache.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 820147a, as suggested: McpCatalog.defsWithMeta returns the listing and its _meta as one value, and every commit — create, the tools/list_changed handler, storeClient on connect and after OAuth — stores that pair rather than re-reading the per-client value. Test: "defsWithMeta hands back the listing and its own _meta as one value". listMeta(client) remains for the catalog tests and is no longer on the commit path.

// layer sanitised them (`[a-zA-Z0-9_-]`), while the declaration carries the
// raw keys, so a raw key with any other character would never count as served
// and the headline would disagree with a report that names no gap. (multi-model review)
const served = declared ? declared.keys.filter((k) => present.has(sanitize(k))).length : present.size

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid overcounting colliding sanitized keys

C5 does not hold when distinct raw declarations sanitize to the same catalog key. For example, with declared keys foo.bar and foo_bar, a listing containing only foo_bar, and an unknown-key report for foo.bar, both declarations pass this predicate because each sanitizes to foo_bar; the headline says 2 of 2 available while the gap line says foo.bar is unavailable. The same collision-prone test is also used for extension keys.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 820147a: served and extServed also exclude any key the engine reports unfulfilled, so of two raw keys that sanitise to one catalog name only the one the report does not name counts. Test: "the headline counts in the catalog's key space, and never a key the report names" — foo.bar + foo_bar over a served datamate_foo_bar with foo.bar reported reads "1 of 2".

…er counts a reported key

Answers the Codex round on the report fixes:
- McpCatalog.defsWithMeta returns the listing and its _meta as one value, and every commit of a listing stores that pair — not a per-client value another refresh may have overwritten while this one was awaiting
- served counts exclude keys the engine reports unfulfilled, so two raw keys that sanitise to one catalog name cannot both count as served
- parseUnfulfilled rejects an entry whose detail is present but not a string, failing closed like the other fields

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
@ralphstodomingo

Copy link
Copy Markdown
Contributor Author

@codex review

Scoped falsification round on the fixes since the last round (45a8d02c8820147aee). For each claim, say whether the code holds it, with a concrete failing scenario where it does not. Please do not re-raise the accepted residuals.

Fix claims

  • F1 — Every commit of a listing stores the tools and the _meta that the SAME listing returned (defsWithMeta): on create, in the tools/list_changed handler, and in storeClient on connect and after OAuth. Two overlapping refreshes for one client can no longer commit one listing's tools with the other's report.
  • F2 — served and extServed count a declared key only when its sanitised form is in the catalog AND the engine does not report it unfulfilled; colliding raw keys therefore cannot both count.
  • F3 — parseUnfulfilled returns undefined for an entry whose detail is present and not a string; a string, an empty string, or an absent detail parses.

Accepted residuals

  • R1 — McpCatalog.listMeta(client) still exists as a per-client read for the catalog tests; nothing on the commit path uses it.
  • R2 — Truncation of detail for the toast stays client-side; bounding and redaction are engine-side.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 820147aee2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +675 to +676
const served = declared
? declared.keys.filter((k) => present.has(sanitize(k)) && !reported.has(k)).length

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Count each sanitized catalog entry at most once

When both colliding raw tools are returned and the engine therefore reports neither as unfulfilled—for example, declarations and listings containing both foo.bar and foo_bar with an empty report—both predicates succeed even though the MCP catalog sanitizes both names to the single foo_bar entry and one overwrites the other. The headline consequently says 2 of 2 available while only one catalog tool is callable, so the counting needs to consume or deduplicate sanitized catalog keys rather than independently counting every matching declaration.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right — with both listed and neither reported, the count was still per declaration. Fixed in 9452109: served and extServed are the number of DISTINCT sanitised catalog entries that are present and unreported, so foo.bar + foo_bar over one datamate_foo_bar count once. Test: "two declarations that sanitise to one catalog entry count once, even with nothing reported" reads "1 of 2".

Two raw keys that sanitise to one catalog name are one callable tool however many the engine lists; served and extension counts are the number of distinct sanitised entries that are present and unreported.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
@ralphstodomingo

Copy link
Copy Markdown
Contributor Author

@codex review

Final scoped falsification round (round 3 of 3) on the fix since the last round (820147aee945210965). For the claim below, say whether the code holds it, with a concrete failing scenario where it does not. Please do not re-raise the accepted residuals.

Fix claim

  • G1 — served and extServed are the number of distinct sanitised catalog entries among the declared keys that are present in the catalog and not reported unfulfilled; declarations that collide after sanitising count once, whether or not the engine listed both.

Accepted residuals

  • R1 — A colliding pair where the engine reports one key and lists the other counts the listed one (the report is the authority on which the served tool stands for).
  • R2 — McpCatalog.listMeta(client) remains as a per-client read for the catalog tests; nothing on the commit path uses it.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9452109654

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// live IDE bridge; when they do they are real capability and the line names
// them, but their absence is the normal no-IDE case, never `missing`.
const extServed = declared ? declared.extensionKeys.filter((k) => present.has(k)).length : 0
const extServed = declared ? servedEntries(declared.extensionKeys) : 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Deduplicate catalog entries across declaration groups

G1 still fails when an ordinary declaration and an extension declaration collide after sanitization. For example, with keys: ["foo.bar"], extensionKeys: ["foo_bar"], one catalog entry datamate_foo_bar, and an empty unfulfilled report, these separate calls each return 1, so the toast reports one integration tool plus one extension tool even though the catalog contains only one callable entry. Track consumed sanitized entries across both counts so a collision is counted only once.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 413fadc: sanitised entries are consumed across both groups, so an ordinary key and an extension key that collide count once, with the ordinary keys. Test: "a collision across the ordinary and extension groups is one entry, counted once" reads "1 of 1" with no extension line.

…nd extension groups

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
@ralphstodomingo

Copy link
Copy Markdown
Contributor Author

Disposition after three Codex rounds — 413fadceb

Three rounds is the cap, so this is the last: the round-3 finding (an ordinary key and an extension key colliding after sanitising were counted in both groups) is fixed in 413fadceb and pinned by a test. No further Codex round; the remaining verification is by hand.

Verified on this head: typecheck clean; test/mcp, test/altimate/workspace, test/altimate/plugin and the tool-race suite pass (the six headers/oauth-auto-connect failures are environmental and identical on the untouched head). Ready for a human review.

Ralph Sto. Domingo and others added 2 commits September 15, 2026 21:40
…w as bare

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Workspace attach: use the engine's unfulfilled-keys report instead of diffing declared vs delivered client-side

2 participants